{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Solution - Part 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's go ahead and just create the descriptors one by one first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numbers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class IntegerField:\n",
    "    def __init__(self, min_, max_):\n",
    "        self._min = min_\n",
    "        self._max = max_\n",
    "\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, numbers.Integral):\n",
    "            raise ValueError(f'{self.prop_name} must be an integer.')\n",
    "        if value < self._min:\n",
    "            raise ValueError(f'{self.prop_name} must be >= {self._min}.')\n",
    "        if value > self._max:\n",
    "            raise ValueError(f'{self.prop_name} must be <= {self._max}')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's just make sure this works as expected:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    age = IntegerField(0, 100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.age = 5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "age must be <= 100\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.age = 200\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But of course, we really need unit testing. So let's write some unit tests to test this functionality. If you're rusty you may want to go back to Project 1 and review the unit test section in there."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import unittest\n",
    "\n",
    "def run_tests(test_class):\n",
    "    suite = unittest.TestLoader().loadTestsFromTestCase(test_class)\n",
    "    runner = unittest.TextTestRunner(verbosity=2)\n",
    "    result = runner.run(suite)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For each test we are going to need a class that defines an instance of our descriptor as an attribute."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could do it this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    class Person:\n",
    "        age = IntegerField(0, 10)\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        p = self.Person()\n",
    "        p.age = 0\n",
    "        self.assertEqual(0, p.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_set_age_ok (__main__.TestIntegerField) ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 1 test in 0.001s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So this kind of testing works just fine, but  our `Person` class `age` is hardcoded to min and max values. We would ideally like to be able to modify those settings for every test (so we can test later with and without those values)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, we'll override the descriptor attribute when we run the test!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    class Person:\n",
    "        age = IntegerField(0, 10)\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        self.Person.age = IntegerField(5, 10)\n",
    "        p = self.Person()\n",
    "        \n",
    "        p.age = 5\n",
    "        self.assertEqual(5, p.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_set_age_ok (__main__.TestIntegerField) ... ERROR\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_ok (__main__.TestIntegerField)\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-11-52de3d0f3544>\", line 11, in test_set_age_ok\n",
      "    p.age = 5\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 16, in __set__\n",
      "    instance.__dict__[self.prop_name] = value\n",
      "AttributeError: 'IntegerField' object has no attribute 'prop_name'\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 1 test in 0.001s\n",
      "\n",
      "FAILED (errors=1)\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hmm... that's not working."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's because we defined the instance of our descriptor outside of a class, so the `__set_name__` method was never called!\n",
    "\n",
    "We could fix this by calling `__set_name__` ourselves, but a cleaner approach would be to do a bit of meta programming. \n",
    "\n",
    "I'll show you both approaches."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    class Person:\n",
    "        pass\n",
    "    \n",
    "    def create_person(self, min_, max_):\n",
    "        self.Person.age = IntegerField(min_, max_)\n",
    "        self.Person.age.__set_name__(Person, 'age')\n",
    "        return self.Person()\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        p = self.create_person(min_, max_)\n",
    "        p.age = 5\n",
    "        self.assertEqual(5, p.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_set_age_ok (__main__.TestIntegerField) ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 1 test in 0.001s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's avoid using this hardcoded `Person` class and this weird patching we had to do by creating a class using a functional approach instead of a declarative one (using the `class` keyword)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We already know that the type of any custom class we create is `type`. It is a metaclass, and classes are actually instances of the `type` metaclass."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `type` metaclass is actually callable, and can be used to create classes, without having to write a `class` definition.\n",
    "\n",
    "The constructor for `type` is: `type(class_name, parent_classes, class_attributes)`\n",
    "where `class_attributes` is a dictionary contain the names and values of the class attributes we want to define for our class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "Person = type('Person', (), {'a': 10})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "type"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "mappingproxy({'a': 10,\n",
       "              '__module__': '__main__',\n",
       "              '__dict__': <attribute '__dict__' of 'Person' objects>,\n",
       "              '__weakref__': <attribute '__weakref__' of 'Person' objects>,\n",
       "              '__doc__': None})"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see we have the same as if we had done this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    age = 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "type"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "mappingproxy({'__module__': '__main__',\n",
       "              'age': 10,\n",
       "              '__dict__': <attribute '__dict__' of 'Person' objects>,\n",
       "              '__weakref__': <attribute '__weakref__' of 'Person' objects>,\n",
       "              '__doc__': None})"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The blank argument we provided is there for inheritance - but we're not using inheritance here, hence the empty tuple."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So let's refactor our test class to use this approach:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    @staticmethod\n",
    "    def create_test_class(min_, max_):\n",
    "        obj = type('TestClass', (), {'age': IntegerField(min_, max_)})\n",
    "        return obj()\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        p = self.create_test_class(min_, max_)\n",
    "        p.age = 5\n",
    "        self.assertEqual(5, p.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_set_age_ok (__main__.TestIntegerField) ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 1 test in 0.001s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, now that this is out of the way, let's continue writing our unit tests:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    @staticmethod\n",
    "    def create_test_class(min_, max_):\n",
    "        obj = type('TestClass', (), {'age': IntegerField(min_, max_)})\n",
    "        return obj()\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        \"\"\"Tests that valid values can be assigned/retrieved\"\"\"\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_values = range(min_, max_)\n",
    "        \n",
    "        for i, value in enumerate(valid_values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_set_age_ok (__main__.TestIntegerField)\n",
      "Tests that valid values can be assigned/retrieved ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 1 test in 0.001s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's add failure tests and a check that we have implemented `__get__` such that using it from the class returns the descriptor instance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    @staticmethod\n",
    "    def create_test_class(min_, max_):\n",
    "        obj = type('TestClass', (), {'age': IntegerField(min_, max_)})\n",
    "        return obj()\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        \"\"\"Tests that valid values can be assigned/retrieved\"\"\"\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_values = range(min_, max_)\n",
    "        \n",
    "        for i, value in enumerate(valid_values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)\n",
    "                \n",
    "    def test_set_age_invalid(self):\n",
    "        \"\"\"Tests that invalid values raise ValueErrors\"\"\"\n",
    "        min_ = -10\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        bad_values = list(range(min_ - 5, min_))\n",
    "        bad_values += list(range(max_ + 1, max_ + 5))\n",
    "        bad_values += [10.5, 1 + 0j, 'abc', (1, 2)]\n",
    "        \n",
    "        for i, value in enumerate(bad_values):\n",
    "            with self.subTest(test_number=i):\n",
    "                with self.assertRaises(ValueError):\n",
    "                    obj.age = value\n",
    "                    \n",
    "    def test_class_get(self):\n",
    "        \"\"\"Tests that class attribute retrieval returns the descriptor instance\"\"\"\n",
    "        obj = self.create_test_class(0, 0)\n",
    "        obj_class = type(obj)\n",
    "        self.assertIsInstance(obj_class.age, IntegerField)\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_class_get (__main__.TestIntegerField)\n",
      "Tests that class attribute retrieval returns the descriptor instance ... ok\n",
      "test_set_age_invalid (__main__.TestIntegerField)\n",
      "Tests that invalid values raise ValueErrors ... ok\n",
      "test_set_age_ok (__main__.TestIntegerField)\n",
      "Tests that valid values can be assigned/retrieved ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 3 tests in 0.002s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so that's our `IntegerField` so far. Let's modify it (and the unit tests) so that we can optionally not specify min/max."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We're actually going to write the tests **first**, run them and make sure they fail, then implement the functionality, re-run the tests and make sure they now pass. (This is an example of test-driven development - we write the tests first, then implement the functionality making sure our tests fail before, and pass after)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestIntegerField(unittest.TestCase):\n",
    "    @staticmethod\n",
    "    def create_test_class(min_, max_):\n",
    "        obj = type('TestClass', (), {'age': IntegerField(min_, max_)})\n",
    "        return obj()\n",
    "        \n",
    "    def test_set_age_ok(self):\n",
    "        \"\"\"Tests that valid values can be assigned/retrieved\"\"\"\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_values = range(min_, max_)\n",
    "        \n",
    "        for i, value in enumerate(valid_values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)\n",
    "                \n",
    "    def test_set_age_invalid(self):\n",
    "        \"\"\"Tests that invalid values raise ValueErrors\"\"\"\n",
    "        min_ = -10\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        bad_values = list(range(min_ - 5, min_))\n",
    "        bad_values += list(range(max_ + 1, max_ + 5))\n",
    "        bad_values += [10.5, 1 + 0j, 'abc', (1, 2)]\n",
    "        \n",
    "        for i, value in enumerate(bad_values):\n",
    "            with self.subTest(test_number=i):\n",
    "                with self.assertRaises(ValueError):\n",
    "                    obj.age = value\n",
    "                    \n",
    "    def test_class_get(self):\n",
    "        \"\"\"Tests that class attribute retrieval returns the descriptor instance\"\"\"\n",
    "        obj = self.create_test_class(0, 0)\n",
    "        obj_class = type(obj)\n",
    "        self.assertIsInstance(obj_class.age, IntegerField)\n",
    "        \n",
    "    def test_set_age_min_only(self):\n",
    "        \"\"\"Tests that we can specify a min value only\"\"\"\n",
    "        min_ = 0\n",
    "        max_ = None\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        values = range(min_, min_ + 100, 10)\n",
    "        for i, value in enumerate(values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)\n",
    "                \n",
    "    def test_set_age_max_only(self):\n",
    "        \"\"\"Tests that we can specify a max value only\"\"\"\n",
    "        min_ = None\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        values = range(max_ - 100, max_, 10)\n",
    "        for i, value in enumerate(values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)\n",
    "                \n",
    "    def test_set_age_no_limits(self):\n",
    "        \"\"\"Tests that we can use IntegerField without any limits at all\"\"\"\n",
    "        min_ = None\n",
    "        max_ = None\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        values = range(-100, 100, 10)\n",
    "        for i, value in enumerate(values):\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.age = value\n",
    "                self.assertEqual(value, obj.age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_class_get (__main__.TestIntegerField)\n",
      "Tests that class attribute retrieval returns the descriptor instance ... ok\n",
      "test_set_age_invalid (__main__.TestIntegerField)\n",
      "Tests that invalid values raise ValueErrors ... ok\n",
      "test_set_age_max_only (__main__.TestIntegerField)\n",
      "Tests that we can specify a max value only ... test_set_age_min_only (__main__.TestIntegerField)\n",
      "Tests that we can specify a min value only ... test_set_age_no_limits (__main__.TestIntegerField)\n",
      "Tests that we can use IntegerField without any limits at all ... test_set_age_ok (__main__.TestIntegerField)\n",
      "Tests that valid values can be assigned/retrieved ... ok\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=0)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=1)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=2)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=3)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=4)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=5)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=6)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=7)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=8)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_max_only (__main__.TestIntegerField) (test_number=9)\n",
      "Tests that we can specify a max value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 58, in test_set_age_max_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=0)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=1)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=2)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=3)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=4)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=5)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=6)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=7)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=8)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_min_only (__main__.TestIntegerField) (test_number=9)\n",
      "Tests that we can specify a min value only\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 47, in test_set_age_min_only\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 14, in __set__\n",
      "    if value > self._max:\n",
      "TypeError: '>' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=0)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=1)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=2)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=3)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=4)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=5)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=6)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=7)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=8)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=9)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=10)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=11)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=12)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=13)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=14)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=15)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=16)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=17)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=18)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "======================================================================\n",
      "ERROR: test_set_age_no_limits (__main__.TestIntegerField) (test_number=19)\n",
      "Tests that we can use IntegerField without any limits at all\n",
      "----------------------------------------------------------------------\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-27-17f43502dd35>\", line 69, in test_set_age_no_limits\n",
      "    obj.age = value\n",
      "  File \"<ipython-input-2-f3204d7bb071>\", line 12, in __set__\n",
      "    if value < self._min:\n",
      "TypeError: '<' not supported between instances of 'int' and 'NoneType'\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 6 tests in 0.006s\n",
      "\n",
      "FAILED (errors=40)\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so now that we have the tests written (and that they all fail), let's implement the functionality and re-test:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "class IntegerField:\n",
    "    def __init__(self, min_, max_):\n",
    "        self._min = min_\n",
    "        self._max = max_\n",
    "\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, numbers.Integral):\n",
    "            raise ValueError(f'{self.prop_name} must be an integer.')\n",
    "        if self._min is not None and value < self._min:\n",
    "            raise ValueError(f'{self.prop_name} must be >= {self._min}.')\n",
    "        if self._max is not None and value > self._max:\n",
    "            raise ValueError(f'{self.prop_name} must be <= {self._max}')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_class_get (__main__.TestIntegerField)\n",
      "Tests that class attribute retrieval returns the descriptor instance ... ok\n",
      "test_set_age_invalid (__main__.TestIntegerField)\n",
      "Tests that invalid values raise ValueErrors ... ok\n",
      "test_set_age_max_only (__main__.TestIntegerField)\n",
      "Tests that we can specify a max value only ... ok\n",
      "test_set_age_min_only (__main__.TestIntegerField)\n",
      "Tests that we can specify a min value only ... ok\n",
      "test_set_age_no_limits (__main__.TestIntegerField)\n",
      "Tests that we can use IntegerField without any limits at all ... ok\n",
      "test_set_age_ok (__main__.TestIntegerField)\n",
      "Tests that valid values can be assigned/retrieved ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 6 tests in 0.006s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestIntegerField)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cool!\n",
    "\n",
    "Now there are some additional tests we could create, like testing if things work when one of the bounds is `0` (this would catch errors such as \n",
    "\n",
    "```\n",
    "if self._min and value < self._min:\n",
    "```\n",
    "\n",
    "which would not work correctly for `_min = 0`\n",
    "\n",
    "But I'll leave this and other tests for you :-)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's move on to the `CharField` descriptor - it's pretty much the same as `IntegerField` so, I'm going to copy/paste and refactor. One main difference is that it does not make sense for `min_` to be a negative number, or to be `None`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CharField:\n",
    "    def __init__(self, min_=None, max_=None):\n",
    "        min_ = min_ or 0  # in case min_ is None\n",
    "        min_ = max(min_, 0)  # replaces negative value with zero\n",
    "        self._min = min_\n",
    "        self._max = max_\n",
    "\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, str):\n",
    "            raise ValueError(f'{self.prop_name} must be a string.')\n",
    "        if self._min is not None and len(value) < self._min:\n",
    "            raise ValueError(f'{self.prop_name} must be >= {self._min} chars.')\n",
    "        if self._max is not None and len(value) > self._max:\n",
    "            raise ValueError(f'{self.prop_name} must be <= {self._max} chars')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's do a quick manual test:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    name = CharField(1, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name must be >= 1 chars.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.name = ''\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name must be <= 10 chars\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.name = 'Python Rocks!'\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.name = 'John'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    name = CharField(-10, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "''"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p = Person()\n",
    "p.name = ''\n",
    "p.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    name = CharField(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"I'm a lumberjack and I'm OK, I sleep all night and I work all day.\""
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p = Person()\n",
    "p.name = \"I'm a lumberjack and I'm OK, I sleep all night and I work all day.\"\n",
    "p.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, we really should write unit tests. These will basically be very similar to the unit tests we created for `IntegerField`, so let's get cracking!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestCharField(unittest.TestCase):\n",
    "    @staticmethod\n",
    "    def create_test_class(min_, max_):\n",
    "        obj = type('TestClass', (), {'name': CharField(min_, max_)})\n",
    "        return obj()\n",
    "        \n",
    "    def test_set_name_ok(self):\n",
    "        \"\"\"Tests that valid values can be assigned/retrieved\"\"\"\n",
    "        min_ = 1\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_lengths = range(min_, max_)\n",
    "        \n",
    "        for i, length in enumerate(valid_lengths):\n",
    "            value = 'a' * length\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.name = value\n",
    "                self.assertEqual(value, obj.name)\n",
    "            \n",
    "    def test_set_name_invalid(self):\n",
    "        \"\"\"Tests that invalid values raise ValueErrors\"\"\"\n",
    "        min_ = 5\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        bad_lengths = list(range(min_ - 5, min_))\n",
    "        bad_lengths += list(range(max_ + 1, max_ + 5))\n",
    "        for i, length in enumerate(bad_lengths):\n",
    "            value = 'a' * length\n",
    "            with self.subTest(test_number=i):\n",
    "                with self.assertRaises(ValueError):\n",
    "                    obj.name = value\n",
    "                    \n",
    "    def test_class_get(self):\n",
    "        \"\"\"Tests that class attribute retrieval returns the descriptor instance\"\"\"\n",
    "        obj = self.create_test_class(0, 0)\n",
    "        obj_class = type(obj)\n",
    "        self.assertIsInstance(obj_class.name, CharField)\n",
    "        \n",
    "    def test_set_name_min_only(self):\n",
    "        \"\"\"Tests that we can specify a min length only\"\"\"\n",
    "        min_ = 0\n",
    "        max_ = None\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_lengths = range(min_, min_ + 100, 10)\n",
    "        for i, length in enumerate(valid_lengths):\n",
    "            value = 'a' * length\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.name = value\n",
    "                self.assertEqual(value, obj.name)\n",
    "    \n",
    "    def test_set_name_min_negative_or_none(self):\n",
    "        \"\"\"Tests that setting a negative or None length results in a zero length\"\"\"\n",
    "        obj = self.create_test_class(-10, 100)\n",
    "        self.assertEqual(type(obj).name._min, 0)\n",
    "        self.assertEqual(type(obj).name._max, 100)\n",
    "        \n",
    "        obj = self.create_test_class(None, None)\n",
    "        self.assertEqual(type(obj).name._min, 0)\n",
    "        self.assertIsNone(type(obj).name._max)\n",
    "        \n",
    "    def test_set_name_max_only(self):\n",
    "        \"\"\"Tests that we can specify a max length only\"\"\"\n",
    "        min_ = None\n",
    "        max_ = 10\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_lengths = range(max_ - 100, max_, 10)\n",
    "        for i, length in enumerate(valid_lengths):\n",
    "            value = 'a' * length\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.name = value\n",
    "                self.assertEqual(value, obj.name)\n",
    "                \n",
    "    def test_set_name_no_limits(self):\n",
    "        \"\"\"Tests that we can use CharField without any limits at all\"\"\"\n",
    "        min_ = None\n",
    "        max_ = None\n",
    "        obj = self.create_test_class(min_, max_)\n",
    "        valid_lengths = range(0, 100, 10)\n",
    "        for i, length in enumerate(valid_lengths):\n",
    "            value = 'a' * length\n",
    "            with self.subTest(test_number=i):\n",
    "                obj.name = value\n",
    "                self.assertEqual(value, obj.name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_class_get (__main__.TestCharField)\n",
      "Tests that class attribute retrieval returns the descriptor instance ... ok\n",
      "test_set_name_invalid (__main__.TestCharField)\n",
      "Tests that invalid values raise ValueErrors ... ok\n",
      "test_set_name_max_only (__main__.TestCharField)\n",
      "Tests that we can specify a max length only ... ok\n",
      "test_set_name_min_negative_or_none (__main__.TestCharField)\n",
      "Tests that setting a negative or None length results in a zero length ... ok\n",
      "test_set_name_min_only (__main__.TestCharField)\n",
      "Tests that we can specify a min length only ... ok\n",
      "test_set_name_no_limits (__main__.TestCharField)\n",
      "Tests that we can use CharField without any limits at all ... ok\n",
      "test_set_name_ok (__main__.TestCharField)\n",
      "Tests that valid values can be assigned/retrieved ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 7 tests in 0.005s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "run_tests(TestCharField)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
